/+
Exception spike.

Copyright (C) 2007 Myron Alexander

This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+/

import std.stdio;
import std.boxer;
import std.string : str = toString, toupper, tolower;
import stdlib = std.c.stdlib;

class WeakObjectReference(T : Object) {

   this (T obj) {
      /* Get a pointer to the object referenced. The pointer is necessary as
       * I do not what type to declare a pointer to a reference (ie T*->T).
       * I tried T*T but that doesn't work. The T** implementation works but
       * out of curiosity, it would be nice to know if something like T*T could
       * be done.
       */
      T* p = cast(T*)obj;

      /* Allocate a non-gc region of memory to store the pointer to the object.
       * The type is T** as it is: memptr -> T* -> obj.
       */
      weakObjRef = cast(T**)stdlib.malloc (p.sizeof);

      /* Set memptr->T* = address of obj reference.
       */
      *weakObjRef = p;

      /* Request that the GC call the unhook delegate when the referenced object
       * is collected. The unhook delegate removes the pointer to the object.
       * This is very important otherwise we would maintain a pointer to a
       * non-existant object.
       */
      obj.notifyRegister (&unhook);
   }

   ~this () {
      /* Remove the object destruction notification delegate as we will no
       * longer maintain a link to that object.
       */
      if (*weakObjRef != null) {
         (cast(T)(*weakObjRef)).notifyUnRegister (&unhook);
         *weakObjRef = null;
      }

      /* Free the non-gc memory allocated in the constructor.
       */
      stdlib.free (weakObjRef);
   }

   T get () {
      return cast(T)(*weakObjRef);
   }

   private void unhook (Object obj) {
      *weakObjRef = null;
   }

   private T** weakObjRef;
}

private static string excVarName (string propertyName) {
   return tolower(propertyName[0..1]) ~ propertyName[1..$];
}

private static string excKeyName (string propertyName) {
   return "K_" ~ toupper (propertyName);
}

private static string excKeyCode (string keyName, string propertyName) {
   return "private static const final "~keyName~" = \""~propertyName~"\";";
}

private static string excHasCode (string keyName, string propertyName) {
   return "bool has"~propertyName~" () {\n"
          "  return hasProperty ("~keyName~");\n"
          "}";
}

private static string excProp ( string propertyType
                              , string propertyName
                              , string defVal       )
{
   string keyName = excKeyName (propertyName);
   string varName = excVarName (propertyName);

   string setCode =
      "typeof(this) set"~propertyName~" ("~propertyType~" "~varName~") {\n"
      "   setProperty ("~keyName~", "~varName~");\n"
      "   return this;\n"
      "}";

   string hasCode = excHasCode (keyName, propertyName);

   string getCode =
      propertyType~" "~varName~" () {\n"
      "   Box v = getProperty ("~keyName~");\n"
      "   if (v.type !is null) {\n"
      "      return unbox!("~propertyType~")(v);\n"
      "   } else {\n"
      "      return "~defVal~";\n"
      "   }\n"
      "}";

   string keyCode = excKeyCode (keyName, propertyName);

   return setCode ~ "\n\n" ~ hasCode ~ "\n\n" ~ getCode ~ "\n\n" ~ keyCode;
}

private static string excProp (string propertyName)
{
   string keyName = excKeyName (propertyName);
   string varName = excVarName (propertyName);

   string setCode =
      "typeof(this) set"~propertyName~"(T) (T "~varName~") {\n"
      "   setProperty ("~keyName~", "~varName~");\n"
      "   return this;\n"
      "}";

   string hasCode = excHasCode (keyName, propertyName);

   string getCode =
      "Box "~varName~" () {\n"
      "   return getProperty ("~keyName~");\n"
      "}";

   string keyCode = excKeyCode (keyName, propertyName);

   return setCode ~ "\n\n" ~ hasCode ~ "\n\n" ~ getCode ~ "\n\n" ~ keyCode;
}

private static string excPropC (string propertyName) {
   return excProp ("string", propertyName, "null");
}

private static string excPropI (string propertyName) {
   return excProp ("int", propertyName, "0");
}

private static string excPropB (string propertyName) {
   return excProp ("bool", propertyName, str (bool.init));
}

public class SqlException : Exception {

   this (char[] msg) {
      super (msg);
   }


   this (char[] msg, SqlException cause) {
      super (msg);
      m_cause = cause;
      cause.setParentCause (this);
   }


   typeof(this) initCause (SqlException cause) {
      m_cause = cause;
      cause.setParentCause (this);
      return this;
   }


   SqlException cause () {
      return m_cause;
   }


   /**
    * Set the SQL text that caused, or was related to the exception.
    *
    * This is not a write property method so as to facilitate method chaining.
    */
   mixin (excPropC ("Sql"));


   /* NOTE: If I change char[] to string, the compiler cannot deduce the
    *       argument types for the template.
    */
   typeof(this) setProperty(T) (char[] property, T value) {
      static if (is (T : Box)) {
         m_propertyBag [property] = value;
      } else {
         m_propertyBag [property] = box (value);
      }
      return this;
   }


   /**
    * Retrieve the value of the named property.
    *
    * The property will be searched through all the bags of the causality chain
    * starting with the head of the chain. If the exception instance is not
    * the head of the chain, the the head of the chain is located as the
    * starting point for the search.
    */
   Box getProperty (string property) {
      /* Find the head of the chain by iterating through the parent cause
       * links.
       */
      if (m_parentCause !is null) {
         SqlException pc = m_parentCause.get ();
         if (pc !is null) {
            return pc.getProperty (property);
         }
      }

      /* If the instance has no parent clause, then start the search.
       */
      return getPropertyDescending (property);
   }


   /**
    * Search for the named property in this class's property bag and, if not
    * found, continue the search through all the cause exception property bags.
    */
   protected Box getPropertyDescending (string property) {
      if (property in m_propertyBag) {
         return m_propertyBag [property];
      } else {
         if (m_cause !is null) {
            return m_cause.getPropertyDescending (property);
         } else {
            return Box();
         }
      }
   }


   bool hasProperty (string property) {
      /* Find the head of the chain by iterating through the parent cause
       * links.
       */
      if (m_parentCause !is null) {
         SqlException pc = m_parentCause.get ();
         if (pc !is null) {
            return pc.hasProperty (property);
         }
      }

      /* If the instance has no parent clause, then start the search.
       */
      return hasPropertyDescending (property);
   }


   protected bool hasPropertyDescending (string property) {
      if (property in m_propertyBag) {
         return true;
      } else {
         if (m_cause !is null) {
            return m_cause.hasPropertyDescending (property);
         } else {
            return false;
         }
      }
   }


   Box[string] propertyBag () {
      return m_propertyBag;
   }


   char[] toString () {

      /* Format properties for output. The properties are listed one to a line
       * of format 'name : value'.
       */
      char[] pstr;

      foreach (p; m_propertyBag.keys.sort) {
         pstr ~= "\n" ~ p ~ ": " ~ m_propertyBag[p].toString ();
      }

      /* Prepend the exception type to the message.
       */
      char[] tmpMsg = this.classinfo.name ~ ": " ~ msg;

      /* If the exception has properties, append the properties to the exception
       * message separated by an empty line.
       */
      if (pstr.length > 0) {
         tmpMsg ~= "\n" ~ pstr;
      }

      /* If the exception has a cause, append the cause descripton separated by
       * an empty line and after the words "caused by: ".
       */
      if (m_cause !is null) {
         tmpMsg ~= "\n\ncaused by: " ~ m_cause.toString ();
      }

      return tmpMsg;
   }


   protected void setParentCause (SqlException parentCause) {
      m_parentCause = new WeakObjectReference!(SqlException) (parentCause);
   }


   protected Box[char[]] m_propertyBag;

   protected SqlException                       m_cause;
   protected WeakObjectReference!(SqlException) m_parentCause;
}

private template ExceptionConstructor () {
   this (char[] msg) {super (msg);}
   this (char[] msg, SqlException cause) {super (msg, cause);}

   /**
    * Override initCause to return the type of the instantiated
    * exception rather than the base exception. This is only necessary so
    * developers can set the cause before the properties.
    */
   override typeof(this) initCause (SqlException cause) {
      super.initCause (cause);
      return this;
   }
}

public class SqlDatabaseException : SqlException {

   mixin ExceptionConstructor;

   mixin (excPropC ("SqlState"));
   mixin (excPropI ("VendorCode"));
   mixin (excPropC ("VendorMsg"));
}

public class SqlDataException : SqlDatabaseException {

   mixin ExceptionConstructor;

   mixin (excPropC ("TypeExpected"));
   mixin (excPropC ("TypeSupplied"));
}


public class SqlProgrammingException : SqlDatabaseException {
   mixin ExceptionConstructor;
}

public class SqlBindException : SqlProgrammingException {
   mixin ExceptionConstructor;

   mixin (excPropC ("BindName"));
   mixin (excPropI ("BindPosition"));
}

public class DbException : SqlException {
   this (char[] msg) {
      super (msg);
   }
}

void main () {
   //~ try {
      //~ try {
         //~ // Within position bind method. The name of the bind parameter is not
         //~ // known, only the position.
         //~ throw (new SqlProgrammingException ("Unable to bind parameter value."))
            //~ .setVendorMsg ("Data type mismatch")
            //~ .setVendorCode (20)
            //~ .setSqlState ("2200G")
            //~ .setSql ("SELECT * FROM TABLE WHERE A = :somevalue")
            //~ .setProperty ("BindPosition", 1)
            //~ ;

      //~ } catch (SqlException e) {
         //~ // Within name lookup method. The name lookup method finds the position
         //~ // of a parameter based on the name. It then calls the position
         //~ // bind method.
         //~ e.setProperty ("BindName", "somevalue");
         //~ throw e;
      //~ }

   //~ } catch (SqlProgrammingException e) {
      //~ writefln ("%s: \n---\n%s\n----", typeid(typeof(e)), e);

   //~ } catch (SqlDatabaseException e) {
      //~ writefln ("%s: \n---\n%s\n----", typeid(typeof(e)), e);

   //~ } catch (Exception e) {
      //~ writefln ("%s: \n---\n%s\n----", typeid(typeof(e)), e);
   //~ }

/+
   try {
      //~ SqlException e =
         //~ (new SqlDataException ("Data type mismatch"))
            //~ .setExpectedType ("int")
            //~ .setSuppliedType ("char[]")
            //~ .setSqlState ("2200G")
            //~ .setVendorMsg ("Data type mismatch")
            //~ .setSql ("SELECT * FROM TABLE WHERE A = :somevalue");

      //~ throw (new SqlProgrammingException ("Unable to bind parameter value", e))
         //~ .setProperty ("BindName", "somevalue")
         //~ .setProperty ("BindPosition", 1);

      //~ throw (new SqlProgrammingException ("Unable to bind parameter value"))
         //~ .initCause (
            //~ (new SqlDataException ("Data type mismatch"))
               //~ .setTypeExpected ("int")
               //~ .setTypeSupplied ("char[]")
               //~ .setVendorMsg ("Data type mismatch")
               //~ .setSql ("SELECT * FROM TABLE WHERE A = :somevalue")
         //~ )
         //~ .setSqlState ("2200G")
         //~ .setVendorCode (42)
         //~ .setProperty ("BindName", "somevalue")
         //~ .setProperty ("BindPosition", 1);

      throw (new SqlBindException ("Unable to bind parameter value"))
         .initCause (
            (new SqlDataException ("Data type mismatch"))
               .setTypeExpected ("int")
               .setTypeSupplied ("char[]")
               .setVendorMsg ("Data type mismatch")
               .setVendorCode (42)
               .setSqlState ("2200G")
               .setSql ("SELECT * FROM TABLE WHERE A = :somevalue")
         )
         .setBindName ("somevalue")
         .setBindPosition (1);

   } catch (SqlException e) {
      writefln ("%s", e);
      writefln ("\n---\nSQL: %s", e.sql);
      writefln ("\n---\nHasTypeExpected: %s", e.hasProperty (SqlDataException.K_TYPEEXPECTED));
      writefln ("\n---\nHasBindUBound: %s", e.hasProperty ("BindUBound"));
   }
+/
   
   SqlDataException e1 = new SqlDataException ("Data exception 1");
   e1.setTypeExpected ("string").setTypeSupplied ("char[]");
   
   SqlProgrammingException e2 = 
      new SqlProgrammingException ("Programming exception e2", e1);
   e2.setProperty ("ParentProp1", "1").setProperty ("ParentProp2", "2");
   
   writefln ("e1: %s", e1.typeExpected);
   writefln ("e1: %s", e1.typeSupplied);
   writefln ("e1: %s", e1.getProperty ("ParentProp1"));
   writefln ("e1: %s", e1.getProperty ("ParentProp2"));
   writefln ("e1: %s", e1.getProperty ("Sql"));

   writefln ("e1: %s", e1.hasTypeExpected);
   writefln ("e1: %s", e1.hasTypeSupplied);
   writefln ("e1: %s", e1.hasProperty ("ParentProp1"));
   writefln ("e1: %s", e1.hasProperty ("ParentProp2"));
   writefln ("e1: %s", e1.hasProperty ("Sql"));

   writefln ("e2: %s", e2.getProperty ("TypeExpected"));
   writefln ("e2: %s", e2.getProperty ("TypeSupplied"));
   writefln ("e2: %s", e2.getProperty ("ParentProp1"));
   writefln ("e2: %s", e2.getProperty ("ParentProp2"));
   writefln ("e2: %s", e2.getProperty ("Sql"));

   writefln ("e2: %s", e2.hasProperty ("TypeExpected"));
   writefln ("e2: %s", e2.hasProperty ("TypeSupplied"));
   writefln ("e2: %s", e2.hasProperty ("ParentProp1"));
   writefln ("e2: %s", e2.hasProperty ("ParentProp2"));
   writefln ("e1: %s", e2.hasProperty ("Sql"));
}
















